Skip to content

enable link to placeholder preview in frontend editing#328

Open
wfehr wants to merge 1 commit intodjango-cms:masterfrom
wfehr:master
Open

enable link to placeholder preview in frontend editing#328
wfehr wants to merge 1 commit intodjango-cms:masterfrom
wfehr:master

Conversation

@wfehr
Copy link

@wfehr wfehr commented Jan 20, 2026

Similar to #282.

I would like to enable a little link tooltip which points to the placeholder of a static-alias.

Little context:
Little hacky - but since we had this behaviour in cms 3 - we are using a similar approach in cms 4 / 5.
We got apphooks which have a content placeholder - in the old cms, this was possible (though not a wanted behaviour).
-> normal content placeholder which could be filled with plugins and the apphook could dynamically throw in data which the plugins could use.
Example: dynamic page output based on given sub paths of the url.
(e.g. /a/ = apphook url -> /a/foo and /a/bar could show different output)

With cms 4 / 5 I can maintain this behaviour by using "dynamic static-alias placeholders" which are built via apphook namespace.
Like {% static_alias "some-prefix-<slugified namespace>" %}.

And to provide the user a better interface in terms of "edit the content" -> with the changes made here they can click the added link and land on the placeholder preview page where they can create/edit drafts.

Summary by Sourcery

Enable optional placeholder preview link for static alias placeholders in frontend editing.

New Features:

  • Add support for a placeholder preview link via the placeholder_preview_link flag on the static_alias template tag to navigate directly to the alias placeholder's admin view from the frontend.

Enhancements:

  • Extend static alias declaration metadata to track whether a placeholder preview link is requested.
  • Guard rendering of the preview link based on toolbar availability, staff status, and non-preview mode to avoid exposing it to anonymous or non-editing users.

Tests:

  • Add comprehensive template tag tests covering when the placeholder preview link is rendered or suppressed based on toolbar state, staff permissions, and extra tag arguments.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 20, 2026

Reviewer's Guide

Adds an optional placeholder_preview_link flag to the static_alias template tag that, when used in staff edit mode (but not preview mode), prepends a styled link pointing to the alias placeholder’s admin preview page; includes tests covering all toolbar/permission states and parsing of the new flag in tag declarations.

Sequence diagram for rendering static_alias with placeholder_preview_link

sequenceDiagram
    actor StaffUser
    participant Browser
    participant CMSFrontend
    participant StaticAliasTag
    participant Toolbar
    participant AdminURL as admin_view_url
    participant AdminPlaceholderPreview

    StaffUser->>Browser: Request page with static_alias placeholder_preview_link
    Browser->>CMSFrontend: HTTP GET /page/
    CMSFrontend->>Toolbar: Initialize toolbar (edit mode or preview mode)
    CMSFrontend->>StaticAliasTag: render_tag(context, static_code, extra_bits, nodelist)

    StaticAliasTag->>Toolbar: check edit_mode_active
    Toolbar-->>StaticAliasTag: edit_mode_active true
    StaticAliasTag->>Toolbar: check preview_mode_active
    Toolbar-->>StaticAliasTag: preview_mode_active false
    StaticAliasTag->>Toolbar: check is_staff
    Toolbar-->>StaticAliasTag: is_staff true

    StaticAliasTag->>AdminURL: admin_view_url(alias)
    AdminURL-->>StaticAliasTag: href to placeholder preview
    StaticAliasTag-->>CMSFrontend: HTML with preview link prepended to placeholder content

    CMSFrontend-->>Browser: Rendered HTML
    Browser->>StaffUser: Shows placeholder content with preview icon link
    StaffUser->>Browser: Click preview link
    Browser->>AdminPlaceholderPreview: HTTP GET href
    AdminPlaceholderPreview-->>Browser: Placeholder preview page
Loading

Class diagram for updated StaticAlias tag and DeclaredStaticAlias

classDiagram
    class StaticAlias {
        +string name
        +Toolbar toolbar
        +Renderer renderer
        +render_tag(context, static_code, extra_bits, nodelist) str
        +get_declaration() DeclaredStaticAlias
    }

    class DeclaredStaticAlias {
        +string static_code
        +bool site
        +bool placeholder_preview_link
    }

    StaticAlias ..> DeclaredStaticAlias : returns
Loading

File-Level Changes

Change Details Files
Support an optional placeholder preview link in the static_alias template tag and its declaration metadata.
  • Extend DeclaredStaticAlias namedtuple to track a placeholder_preview_link boolean flag.
  • Parse extra_bits in StaticAlias.get_declaration to detect the presence of the placeholder_preview_link argument alongside the existing site flag.
  • Return the extended DeclaredStaticAlias instance including placeholder_preview_link in tag declarations.
djangocms_alias/templatetags/djangocms_alias_tags.py
Render a styled placeholder preview link for static aliases in appropriate toolbar states.
  • Within StaticAlias.render_tag, when placeholder_preview_link is requested and a toolbar exists, staff is active, and preview mode is off, compute the admin URL for the alias using admin_view_url.
  • Build and prepend an HTML span with a positioned cms-icon-view link pointing at the admin URL, using inline styles and a dedicated CSS class name.
  • Ensure normal static alias content rendering remains unchanged when conditions are not met.
djangocms_alias/templatetags/djangocms_alias_tags.py
Test static_alias rendering behavior with and without the placeholder preview link under different toolbar and user conditions.
  • Add a template-tag test that exercises both regular static_alias rendering and the new placeholder_preview_link variant.
  • Verify the preview link is not rendered when the flag is omitted, when toolbar preview_mode_active is true, when the user is non-staff, or when no toolbar is attached to the request.
  • Verify the preview link is rendered and detected by its CSS class when in edit mode as staff and that additional extra_bits like site do not interfere.
  • Assert that anonymous requests never see the preview link even if the flag is present.
tests/test_templatetags.py

Possibly linked issues

  • #0: The PR implements the requested frontend placeholder preview link for static-alias via the new placeholder_preview_link option.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The HTML for the preview link is embedded as a large inline string with inline styles; consider moving styling to CSS and building the markup via format_html (or a small template/utility) to improve readability and avoid unintended whitespace.
  • In DeclaredStaticAlias, the new placeholder_preview_link field is set but not referenced elsewhere; if it's not needed for downstream consumers, you might remove it to keep the declaration minimal and avoid confusion.
  • When generating the href with admin_view_url(alias) inside the f-string, using format_html for the whole link block would be safer and more consistent with Django’s escaping conventions rather than interpolating raw strings.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The HTML for the preview link is embedded as a large inline string with inline styles; consider moving styling to CSS and building the markup via `format_html` (or a small template/utility) to improve readability and avoid unintended whitespace.
- In `DeclaredStaticAlias`, the new `placeholder_preview_link` field is set but not referenced elsewhere; if it's not needed for downstream consumers, you might remove it to keep the declaration minimal and avoid confusion.
- When generating the `href` with `admin_view_url(alias)` inside the f-string, using `format_html` for the whole link block would be safer and more consistent with Django’s escaping conventions rather than interpolating raw strings.

## Individual Comments

### Comment 1
<location> `tests/test_templatetags.py:454-461` </location>
<code_context>
+
+            request.toolbar.preview_mode_active = False
+
+        with self.subTest("preview-link enabled in edit-mode"):
+            output = self.render_template_obj(
+                alias_link_enabled_template,
+                {},
+                request,
+            )
+            self.assertNotEqual(alias_content, output)
+            self.assertIn(alias_preview_link, output)
+
+            # check that other "extra_bits" don't interfere with functionality
</code_context>

<issue_to_address>
**suggestion (testing):** Add a more specific assertion on the preview link element to ensure we’re testing the exact behavior and not just partial HTML.

Currently we only check that `alias_preview_link` is a substring of the rendered output. Please tighten this by asserting that the full `<a>` element is present with the expected CSS class, and that its `href` targets the alias admin/preview URL (e.g., contains the alias ID or a known path segment). This will make the test more robust and clearly specify the expected markup.

Suggested implementation:

```python
        with self.subTest("preview-link enabled in edit-mode"):
            output = self.render_template_obj(
                alias_link_enabled_template,
                {},
                request,
            )
            self.assertNotEqual(alias_content, output)
            # basic sanity check that the preview link markup is present
            self.assertIn(alias_preview_link, output)

            # assert that the full <a> element is rendered with the expected class and href
            preview_link_match = re.search(
                r'<a[^>]+class="[^"]*\bplaceholder_preview_link\b[^"]*"[^>]+href="(?P<href>[^"]+)"',
                output,
            )
            self.assertIsNotNone(
                preview_link_match,
                msg="Expected an <a> tag with class containing 'placeholder_preview_link' in the rendered output.",
            )
            preview_href = preview_link_match.group("href")
            self.assertIn(
                "/admin/cms/alias/",
                preview_href,
                msg="Preview link href should point to the alias admin preview URL.",
            )
            # ensure the href targets this specific alias
            self.assertIn(
                str(alias.pk),
                preview_href,
                msg="Preview link href should contain the alias primary key.",
            )

            # check that other "extra_bits" don't interfere with functionality
            output = self.render_template_obj(
                alias_link_enabled_template.replace(
                    "placeholder_preview_link",
                    "site placeholder_preview_link",
                ),
                {},
                request,
            )
            self.assertNotEqual(alias_content, output)
            # basic sanity check that the preview link markup is present
            self.assertIn(alias_preview_link, output)

            # assert that the full <a> element is still rendered correctly
            preview_link_match = re.search(
                r'<a[^>]+class="[^"]*\bplaceholder_preview_link\b[^"]*"[^>]+href="(?P<href>[^"]+)"',
                output,
            )
            self.assertIsNotNone(
                preview_link_match,
                msg=(
                    "Expected an <a> tag with class containing 'placeholder_preview_link' "
                    "in the rendered output when extra_bits are present."
                ),
            )
            preview_href = preview_link_match.group("href")
            self.assertIn(
                "/admin/cms/alias/",
                preview_href,
                msg="Preview link href should point to the alias admin preview URL.",
            )
            self.assertIn(
                str(alias.pk),
                preview_href,
                msg="Preview link href should contain the alias primary key.",
            )

```

1. Ensure `re` is imported at the top of `tests/test_templatetags.py`:
   - If not already present, add `import re`.
2. The above assertions assume an `alias` object (with `.pk`) is already available in the test method’s scope, which is typical for this kind of test. If the variable name differs (e.g., `alias_obj` or `self.alias`), replace `alias.pk` with the correct reference.
3. If the actual admin preview URL pattern differs from `/admin/cms/alias/`, adjust the `self.assertIn("/admin/cms/alias/", preview_href, ...)` check to match the real path segment used in your project’s URL configuration.
</issue_to_address>

### Comment 2
<location> `tests/test_templatetags.py:497-505` </location>
<code_context>
+
+            request.toolbar = toolbar
+
+        with self.subTest("preview-link hidden for anonymous user"):
+            anon_request = self.get_request("/")
+            anon_request.user = AnonymousUser()
+            output = self.render_template_obj(
+                alias_link_enabled_template,
+                {},
+                anon_request,
+            )
+            self.assertNotIn(alias_preview_link, output)
</code_context>

<issue_to_address>
**suggestion (testing):** Also assert the alias content is still rendered for anonymous users, not only that the link is hidden.

This only asserts that the preview link is absent. Please also assert that the alias content is present (e.g. `self.assertEqual(alias_content, output)` or a suitable containment check) so we verify the link is hidden without suppressing the content for anonymous users.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@fsbraun
Copy link
Member

fsbraun commented Jan 20, 2026

@wfehr This is certainly an interesting proposal. I like the idea that you can go to the edit(?) view of a static placeholder similarly as to editing a plugin alternatively to using the structure board (I assume you do not use it for static aliases?).

I think we would need some discussion on how to consistently implement this, and have a consistent UI here. Maybe the right place for that would be the djangocms-alias workgroup on Discord?

But first I need to understand your use case better.

  • Apphooked models can have placeholders just as in django CMS 3 - or are you referring to the page's placeholder on the apphook root (see your discussion apphook and content placeholder django-cms#8353)?
  • Also, the apphook (root or other) is managing the Django view, so it can "throw in data" by populating the render context.
  • Now, where you lose me, is what happens in those child views /a/foo or /a/bar.
    • Do the apphook "leaves" (/a/foo or /a/bar) have models attached to them? Do those models have a PlaceholderRelationField?
    • If so, why do you need static aliases?
    • If not, are you trying to render the apphook root's placeholders? (If so, this (a) is unconventional but (b) will be easily possible by setting the toolbar object to the apphook's content object - no workaround needed)

I'm sorry if these questions are strange. I just want to wrap my head around what you're trying to achieve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants